[bucketId].tsx 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. import { useParams } from 'common'
  2. import { ChevronDown, FolderOpen, Settings, Shield, Trash2 } from 'lucide-react'
  3. import Link from 'next/link'
  4. import { useRouter } from 'next/router'
  5. import { parseAsBoolean, useQueryState } from 'nuqs'
  6. import { useEffect } from 'react'
  7. import { toast } from 'sonner'
  8. import {
  9. Badge,
  10. Button,
  11. DropdownMenu,
  12. DropdownMenuContent,
  13. DropdownMenuItem,
  14. DropdownMenuSeparator,
  15. DropdownMenuTrigger,
  16. Tooltip,
  17. TooltipContent,
  18. TooltipTrigger,
  19. } from 'ui'
  20. import { DeleteBucketModal } from '@/components/interfaces/Storage/DeleteBucketModal'
  21. import { EditBucketModal } from '@/components/interfaces/Storage/EditBucketModal'
  22. import { EmptyBucketModal } from '@/components/interfaces/Storage/EmptyBucketModal'
  23. import { useSelectedBucket } from '@/components/interfaces/Storage/FilesBuckets/useSelectedBucket'
  24. import { PublicBucketWarning } from '@/components/interfaces/Storage/PublicBucketWarning'
  25. import { PUBLIC_BUCKET_TOOLTIP } from '@/components/interfaces/Storage/Storage.constants'
  26. import StorageBucketsError from '@/components/interfaces/Storage/StorageBucketsError'
  27. import { StorageExplorer } from '@/components/interfaces/Storage/StorageExplorer/StorageExplorer'
  28. import { useBucketPolicyCount } from '@/components/interfaces/Storage/useBucketPolicyCount'
  29. import DefaultLayout from '@/components/layouts/DefaultLayout'
  30. import { PageLayout } from '@/components/layouts/PageLayout/PageLayout'
  31. import StorageLayout from '@/components/layouts/StorageLayout/StorageLayout'
  32. import { StorageExplorerStateContextProvider } from '@/state/storage-explorer'
  33. import type { NextPageWithLayout } from '@/types'
  34. const BucketPage: NextPageWithLayout = () => {
  35. const router = useRouter()
  36. const { bucketId, ref } = useParams()
  37. const { data: bucket, error, isSuccess, isError } = useSelectedBucket()
  38. const [showEditModal, setShowEditModal] = useQueryState(
  39. 'edit',
  40. parseAsBoolean.withDefault(false).withOptions({ history: 'push', clearOnDefault: true })
  41. )
  42. const [showEmptyModal, setShowEmptyModal] = useQueryState(
  43. 'empty',
  44. parseAsBoolean.withDefault(false).withOptions({ history: 'push', clearOnDefault: true })
  45. )
  46. const [showDeleteModal, setShowDeleteModal] = useQueryState(
  47. 'delete',
  48. parseAsBoolean.withDefault(false).withOptions({ history: 'push', clearOnDefault: true })
  49. )
  50. const { getPolicyCount } = useBucketPolicyCount()
  51. const policyCount = bucket ? getPolicyCount(bucket.id) : 0
  52. useEffect(() => {
  53. if (isSuccess && !bucket) {
  54. toast.info(`Bucket "${bucketId}" does not exist in your project`)
  55. router.push(`/project/${ref}/storage/files`)
  56. }
  57. // eslint-disable-next-line react-hooks/exhaustive-deps
  58. }, [isSuccess])
  59. if (isError) {
  60. return <StorageBucketsError error={error} />
  61. }
  62. return (
  63. <StorageExplorerStateContextProvider key={`storage-explorer-state-${ref}`}>
  64. <PageLayout
  65. size="full"
  66. isCompact
  67. className="[&>div:first-child]:border-b-0!" // Override the border-b from ScaffoldContainer
  68. title={
  69. <div className="flex items-center gap-2 min-w-0 flex-1">
  70. <span className="truncate">{bucketId}</span>
  71. {bucket?.public && (
  72. <Tooltip>
  73. <TooltipTrigger asChild>
  74. <Badge variant="warning" className="flex shrink-0">
  75. Public
  76. </Badge>
  77. </TooltipTrigger>
  78. <TooltipContent side="bottom">{PUBLIC_BUCKET_TOOLTIP}</TooltipContent>
  79. </Tooltip>
  80. )}
  81. </div>
  82. }
  83. breadcrumbs={[
  84. {
  85. label: 'Files',
  86. href: `/project/${ref}/storage/files`,
  87. },
  88. {
  89. label: 'Buckets',
  90. href: `/project/${ref}/storage/files`,
  91. },
  92. ]}
  93. primaryActions={
  94. <>
  95. <Button
  96. asChild
  97. type="default"
  98. icon={<Shield size={14} />}
  99. iconRight={
  100. policyCount > 0 ? (
  101. <span className="w-4 h-4 bg-surface-200 text-foreground-light text-xs rounded-full flex items-center justify-center font-medium">
  102. {policyCount}
  103. </span>
  104. ) : undefined
  105. }
  106. >
  107. <Link
  108. href={`/project/${ref}/storage/files/policies?search=${encodeURIComponent(bucket?.name ?? '')}`}
  109. >
  110. Policies
  111. </Link>
  112. </Button>
  113. <DropdownMenu>
  114. <DropdownMenuTrigger asChild>
  115. <Button type="default" iconRight={<ChevronDown size={14} />}>
  116. Edit bucket
  117. </Button>
  118. </DropdownMenuTrigger>
  119. <DropdownMenuContent align="end" className="w-40">
  120. <DropdownMenuItem
  121. className="flex items-center space-x-2"
  122. onClick={() => setShowEditModal(true)}
  123. >
  124. <Settings size={12} />
  125. <p>Bucket settings</p>
  126. </DropdownMenuItem>
  127. <DropdownMenuSeparator />
  128. <DropdownMenuItem
  129. className="flex items-center space-x-2"
  130. onClick={() => setShowEmptyModal(true)}
  131. >
  132. <FolderOpen size={12} />
  133. <p>Empty bucket</p>
  134. </DropdownMenuItem>
  135. <DropdownMenuItem
  136. className="flex items-center space-x-2"
  137. onClick={() => setShowDeleteModal(true)}
  138. >
  139. <Trash2 size={12} />
  140. <p>Delete bucket</p>
  141. </DropdownMenuItem>
  142. </DropdownMenuContent>
  143. </DropdownMenu>
  144. </>
  145. }
  146. >
  147. <div className="flex-1 min-h-0 px-6 pb-6 flex flex-col gap-4">
  148. {ref && bucketId && <PublicBucketWarning projectRef={ref} bucketId={bucketId} />}
  149. <div className="flex-1 min-h-0">
  150. <StorageExplorer />
  151. </div>
  152. </div>
  153. </PageLayout>
  154. {bucket && (
  155. <>
  156. <EditBucketModal
  157. visible={showEditModal}
  158. bucket={bucket}
  159. onClose={() => setShowEditModal(false)}
  160. />
  161. <EmptyBucketModal
  162. visible={showEmptyModal}
  163. bucket={bucket}
  164. onClose={() => setShowEmptyModal(false)}
  165. />
  166. <DeleteBucketModal
  167. visible={showDeleteModal}
  168. bucket={bucket}
  169. onClose={() => setShowDeleteModal(false)}
  170. />
  171. </>
  172. )}
  173. </StorageExplorerStateContextProvider>
  174. )
  175. }
  176. BucketPage.getLayout = (page) => (
  177. <DefaultLayout>
  178. <StorageLayout title="Buckets">{page}</StorageLayout>
  179. </DefaultLayout>
  180. )
  181. export default BucketPage